const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const yaml = require('js-yaml');
const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler');

/**
 * Handler for custom content (custom.yaml)
 * Installs custom agents and workflows without requiring a full module structure
 */
class CustomHandler {
  constructor() {
    this.fileOps = new FileOps();
    this.xmlHandler = new XmlHandler();
  }

  /**
   * Find all custom.yaml files in the project
   * @param {string} projectRoot - Project root directory
   * @returns {Array} List of custom content paths
   */
  async findCustomContent(projectRoot) {
    const customPaths = [];

    // Helper function to recursively scan directories
    async function scanDirectory(dir, excludePaths = []) {
      try {
        const entries = await fs.readdir(dir, { withFileTypes: true });

        for (const entry of entries) {
          const fullPath = path.join(dir, entry.name);

          // Skip hidden directories and common exclusions
          if (
            entry.name.startsWith('.') ||
            entry.name === 'node_modules' ||
            entry.name === 'dist' ||
            entry.name === 'build' ||
            entry.name === '.git' ||
            entry.name === 'bmad'
          ) {
            continue;
          }

          // Skip excluded paths
          if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) {
            continue;
          }

          if (entry.isDirectory()) {
            // Recursively scan subdirectories
            await scanDirectory(fullPath, excludePaths);
          } else if (entry.name === 'custom.yaml') {
            // Found a custom.yaml file
            customPaths.push(fullPath);
          } else if (
            entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
            // Skip if it's in src/modules (those are standard modules)
            !fullPath.includes(path.join('src', 'modules'))
          ) {
            customPaths.push(fullPath);
          }
        }
      } catch {
        // Ignore errors (e.g., permission denied)
      }
    }

    // Scan the entire project, but exclude source directories
    await scanDirectory(projectRoot, [path.join(projectRoot, 'src'), path.join(projectRoot, 'tools'), path.join(projectRoot, 'test')]);

    return customPaths;
  }

  /**
   * Get custom content info from a custom.yaml or module.yaml file
   * @param {string} configPath - Path to config file
   * @param {string} projectRoot - Project root directory for calculating relative paths
   * @returns {Object|null} Custom content info
   */
  async getCustomInfo(configPath, projectRoot = null) {
    try {
      const configContent = await fs.readFile(configPath, 'utf8');

      // Try to parse YAML with error handling
      let config;
      try {
        config = yaml.load(configContent);
      } catch (parseError) {
        console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
        return null;
      }

      // Check if this is an module.yaml (module) or custom.yaml (custom content)
      const isInstallConfig = configPath.endsWith('module.yaml');
      const configDir = path.dirname(configPath);

      // Use provided projectRoot or fall back to process.cwd()
      const basePath = projectRoot || process.cwd();
      const relativePath = path.relative(basePath, configDir);

      return {
        id: config.code || 'unknown-code',
        name: config.name,
        description: config.description || '',
        path: configDir,
        relativePath: relativePath,
        defaultSelected: config.default_selected === true,
        config: config,
        isInstallConfig: isInstallConfig, // Track which type this is
      };
    } catch (error) {
      console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
      return null;
    }
  }

  /**
   * Install custom content
   * @param {string} customPath - Path to custom content directory
   * @param {string} bmadDir - Target bmad directory
   * @param {Object} config - Configuration from custom.yaml
   * @param {Function} fileTrackingCallback - Optional callback to track installed files
   * @returns {Object} Installation result
   */
  async install(customPath, bmadDir, config, fileTrackingCallback = null) {
    const results = {
      agentsInstalled: 0,
      workflowsInstalled: 0,
      filesCopied: 0,
      preserved: 0,
      errors: [],
    };

    try {
      // Create custom directories in bmad
      const bmadCustomDir = path.join(bmadDir, 'custom');
      const bmadAgentsDir = path.join(bmadCustomDir, 'agents');
      const bmadWorkflowsDir = path.join(bmadCustomDir, 'workflows');

      await fs.ensureDir(bmadCustomDir);
      await fs.ensureDir(bmadAgentsDir);
      await fs.ensureDir(bmadWorkflowsDir);

      // Process agents - compile and copy agents
      const agentsDir = path.join(customPath, 'agents');
      if (await fs.pathExists(agentsDir)) {
        await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);

        // Count agent files
        const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
        results.agentsInstalled = agentFiles.length;
      }

      // Process workflows - copy entire workflows directory structure
      const workflowsDir = path.join(customPath, 'workflows');
      if (await fs.pathExists(workflowsDir)) {
        await this.copyDirectory(workflowsDir, bmadWorkflowsDir, results, fileTrackingCallback, config);

        // Count workflow files
        const workflowFiles = await this.findFilesRecursively(workflowsDir, ['.md']);
        results.workflowsInstalled = workflowFiles.length;
      }

      // Process any additional files at root
      const entries = await fs.readdir(customPath, { withFileTypes: true });
      for (const entry of entries) {
        if (entry.isFile() && entry.name !== 'custom.yaml' && !entry.name.startsWith('.') && !entry.name.endsWith('.md')) {
          // Skip .md files at root as they're likely docs
          const sourcePath = path.join(customPath, entry.name);
          const targetPath = path.join(bmadCustomDir, entry.name);

          try {
            // Check if file already exists
            if (await fs.pathExists(targetPath)) {
              // File already exists, preserve it
              results.preserved = (results.preserved || 0) + 1;
            } else {
              await fs.copy(sourcePath, targetPath);
              results.filesCopied++;

              if (fileTrackingCallback) {
                fileTrackingCallback(targetPath);
              }
            }
          } catch (error) {
            results.errors.push(`Failed to copy file ${entry.name}: ${error.message}`);
          }
        }
      }
    } catch (error) {
      results.errors.push(`Installation failed: ${error.message}`);
    }

    return results;
  }

  /**
   * Find all files with specific extensions recursively
   * @param {string} dir - Directory to search
   * @param {Array} extensions - File extensions to match
   * @returns {Array} List of matching files
   */
  async findFilesRecursively(dir, extensions) {
    const files = [];

    async function search(currentDir) {
      const entries = await fs.readdir(currentDir, { withFileTypes: true });

      for (const entry of entries) {
        const fullPath = path.join(currentDir, entry.name);

        if (entry.isDirectory()) {
          await search(fullPath);
        } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
          files.push(fullPath);
        }
      }
    }

    await search(dir);
    return files;
  }

  /**
   * Recursively copy a directory
   * @param {string} sourceDir - Source directory
   * @param {string} targetDir - Target directory
   * @param {Object} results - Results object to update
   * @param {Function} fileTrackingCallback - Optional callback
   * @param {Object} config - Configuration for placeholder replacement
   */
  async copyDirectory(sourceDir, targetDir, results, fileTrackingCallback, config) {
    await fs.ensureDir(targetDir);
    const entries = await fs.readdir(sourceDir, { withFileTypes: true });

    for (const entry of entries) {
      const sourcePath = path.join(sourceDir, entry.name);
      const targetPath = path.join(targetDir, entry.name);

      if (entry.isDirectory()) {
        await this.copyDirectory(sourcePath, targetPath, results, fileTrackingCallback, config);
      } else {
        try {
          // Check if file already exists
          if (await fs.pathExists(targetPath)) {
            // File already exists, preserve it
            results.preserved = (results.preserved || 0) + 1;
          } else {
            // Copy with placeholder replacement for text files
            const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
            if (textExtensions.some((ext) => entry.name.endsWith(ext))) {
              // Read source content
              let content = await fs.readFile(sourcePath, 'utf8');

              // Replace placeholders
              content = content.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
              content = content.replaceAll('{user_name}', config.user_name || 'User');
              content = content.replaceAll('{communication_language}', config.communication_language || 'English');
              content = content.replaceAll('{output_folder}', config.output_folder || 'docs');

              // Write to target
              await fs.ensureDir(path.dirname(targetPath));
              await fs.writeFile(targetPath, content, 'utf8');
            } else {
              // Copy binary files as-is
              await fs.copy(sourcePath, targetPath);
            }

            results.filesCopied++;
            if (fileTrackingCallback) {
              fileTrackingCallback(targetPath);
            }
          }

          if (entry.name.endsWith('.md')) {
            results.workflowsInstalled++;
          }
        } catch (error) {
          results.errors.push(`Failed to copy ${entry.name}: ${error.message}`);
        }
      }
    }
  }

  /**
   * Compile .agent.yaml files to .md format and handle sidecars
   * @param {string} sourceAgentsPath - Source agents directory
   * @param {string} targetAgentsPath - Target agents directory
   * @param {string} bmadDir - BMAD installation directory
   * @param {Object} config - Configuration for placeholder replacement
   * @param {Function} fileTrackingCallback - Optional callback to track installed files
   * @param {Object} results - Results object to update
   */
  async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
    // Get all .agent.yaml files recursively
    const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);

    for (const agentFile of agentFiles) {
      const relativePath = path.relative(sourceAgentsPath, agentFile);
      const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));

      await fs.ensureDir(targetDir);

      const agentName = path.basename(agentFile, '.agent.yaml');
      const targetMdPath = path.join(targetDir, `${agentName}.md`);
      // Use the actual bmadDir if available (for when installing to temp dir)
      const actualBmadDir = config._bmadDir || bmadDir;
      const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);

      // Read and compile the YAML
      try {
        const yamlContent = await fs.readFile(agentFile, 'utf8');
        const { compileAgent } = require('../../../lib/agent/compiler');

        // Create customize template if it doesn't exist
        if (!(await fs.pathExists(customizePath))) {
          const { getSourcePath } = require('../../../lib/project-root');
          const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
          if (await fs.pathExists(genericTemplatePath)) {
            // Copy with placeholder replacement
            let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
            templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
            await fs.writeFile(customizePath, templateContent, 'utf8');
            console.log(chalk.dim(`  Created customize: custom-${agentName}.customize.yaml`));
          }
        }

        // Compile the agent
        const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });

        // Replace placeholders in the compiled content
        let processedXml = xml;
        processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
        processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
        processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
        processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');

        // Write the compiled MD file
        await fs.writeFile(targetMdPath, processedXml, 'utf8');

        // Check if agent has sidecar
        let hasSidecar = false;
        try {
          const yamlLib = require('yaml');
          const agentYaml = yamlLib.parse(yamlContent);
          hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
        } catch {
          // Continue without sidecar processing
        }

        // Copy sidecar files if agent has hasSidecar flag
        if (hasSidecar && config.agent_sidecar_folder) {
          const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');

          // Resolve agent sidecar folder path
          const projectDir = path.dirname(bmadDir);
          const resolvedSidecarFolder = config.agent_sidecar_folder
            .replaceAll('{project-root}', projectDir)
            .replaceAll('{bmad_folder}', path.basename(bmadDir));

          // Create sidecar directory for this agent
          const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
          await fs.ensureDir(agentSidecarDir);

          // Copy sidecar files
          const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);

          if (sidecarResult.copied.length > 0) {
            console.log(chalk.dim(`    Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
          }
          if (sidecarResult.preserved.length > 0) {
            console.log(chalk.dim(`    Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
          }
        }

        // Track the file
        if (fileTrackingCallback) {
          fileTrackingCallback(targetMdPath);
        }

        console.log(
          chalk.dim(
            `    Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
          ),
        );
      } catch (error) {
        console.warn(chalk.yellow(`    Failed to compile agent ${agentName}:`, error.message));
        results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
      }
    }
  }
}

module.exports = { CustomHandler };
